/*
 * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
 * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
 * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
 * Vestibulum commodo. Ut rhoncus gravida arcu.
 */

package com.darly.dlcommon.common.bolts.links;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.SparseArray;

import com.darly.dlcommon.common.bolts.tasks.Task;
import com.darly.dlcommon.common.bolts.tasks.iface.Continuation;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Represents a pending request to navigate to an App Link. Most developers will simply use
 * {@link #navigateInBackground(Context, Uri)} to open a URL, but
 * developers can build custom requests with additional navigation and app data attached to them
 * by creating AppLinkNavigations themselves.
 */
public class AppLinkNavigation {

    private static final String KEY_NAME_USER_AGENT = "user_agent";
    private static final String KEY_NAME_VERSION = "version";
    private static final String KEY_NAME_REFERER_APP_LINK = "referer_app_link";
    private static final String KEY_NAME_REFERER_APP_LINK_APP_NAME = "app_name";
    private static final String KEY_NAME_REFERER_APP_LINK_PACKAGE = "package";
    private static final String VERSION = "1.0";

    private static AppLinkResolver defaultResolver;

    /**
     * The result of calling {@link #navigate(Context)} on an
     * {@link AppLinkNavigation}.
     */
    public static enum NavigationResult {
        /**
         * Indicates that the navigation failed and no app was opened.
         */
        FAILED("failed", false),
        /**
         * Indicates that the navigation succeeded by opening the URL in the browser.
         */
        WEB("web", true),
        /**
         * Indicates that the navigation succeeded by opening the URL in an app on the device.
         */
        APP("app", true);

        private String code;
        private boolean succeeded;

        public String getCode() {
            return code;
        }

        public boolean isSucceeded() {
            return succeeded;
        }

        NavigationResult(String code, boolean success) {
            this.code = code;
            this.succeeded = success;
        }
    }

    private final AppLink appLink;
    private final Bundle extras;
    private final Bundle appLinkData;

    /**
     * Creates an AppLinkNavigation with the given link, extras, and App Link data.
     *
     * @param appLink     the AppLink being navigated to.
     * @param extras      the extras to include in the App Link navigation.
     * @param appLinkData additional App Link data for the navigation.
     */
    public AppLinkNavigation(AppLink appLink, Bundle extras, Bundle appLinkData) {
        if (appLink == null) {
            throw new IllegalArgumentException("appLink must not be null.");
        }
        if (extras == null) {
            extras = new Bundle();
        }
        if (appLinkData == null) {
            appLinkData = new Bundle();
        }
        this.appLink = appLink;
        this.extras = extras;
        this.appLinkData = appLinkData;
    }

    /**
     * @return the App Link to navigate to.
     */
    public AppLink getAppLink() {
        return appLink;
    }

    /**
     * Gets the al_applink_data for the AppLinkNavigation. This will generally contain data common
     * to navigation attempts such as back-links, user agents, and other information that may be used
     * in routing and handling an App Link request.
     *
     * @return the App Link data.
     */
    public Bundle getAppLinkData() {
        return appLinkData;
    }

    /**
     * The extras for the AppLinkNavigation. This will generally contain application-specific data
     * that should be passed along with the request, such as advertiser or affiliate IDs or other such
     * metadata relevant on this device.
     *
     * @return the extras for the AppLinkNavigation.
     */
    public Bundle getExtras() {
        return extras;
    }

    /**
     * Creates a bundle containing the final, constructed App Link data to be used in navigation.
     */
    private Bundle buildAppLinkDataForNavigation(Context context) {
        Bundle data = new Bundle();
        Bundle refererAppLinkData = new Bundle();
        if (context != null) {
            String refererAppPackage = context.getPackageName();
            if (refererAppPackage != null) {
                refererAppLinkData.putString(KEY_NAME_REFERER_APP_LINK_PACKAGE, refererAppPackage);
            }
            ApplicationInfo appInfo = context.getApplicationInfo();
            if (appInfo != null) {
                String refererAppName = context.getString(appInfo.labelRes);
                if (refererAppName != null) {
                    refererAppLinkData.putString(KEY_NAME_REFERER_APP_LINK_APP_NAME, refererAppName);
                }
            }
        }
        data.putAll(getAppLinkData());
        data.putString(AppLinks.KEY_NAME_TARGET, getAppLink().getSourceUrl().toString());
        data.putString(KEY_NAME_VERSION, VERSION);
        data.putString(KEY_NAME_USER_AGENT, "Bolts Android " + Bolts.VERSION);
        data.putBundle(KEY_NAME_REFERER_APP_LINK, refererAppLinkData);
        data.putBundle(AppLinks.KEY_NAME_EXTRAS, getExtras());
        return data;
    }

    /**
     * Gets a JSONObject-compatible value for the given object.
     */
    private Object getJSONValue(Object value) throws JSONException {
        if (value instanceof Bundle) {
            return getJSONForBundle((Bundle) value);
        } else if (value instanceof CharSequence) {
            return value.toString();
        } else if (value instanceof List) {
            JSONArray array = new JSONArray();
            for (Object listValue : (List<?>) value) {
                array.put(getJSONValue(listValue));
            }
            return array;
        } else if (value instanceof SparseArray) {
            JSONArray array = new JSONArray();
            SparseArray<?> sparseValue = (SparseArray<?>) value;
            for (int i = 0; i < sparseValue.size(); i++) {
                array.put(sparseValue.keyAt(i), getJSONValue(sparseValue.valueAt(i)));
            }
            return array;
        } else if (value instanceof Character) {
            return value.toString();
        } else if (value instanceof Boolean) {
            return value;
        } else if (value instanceof Number) {
            if (value instanceof Double || value instanceof Float) {
                return ((Number) value).doubleValue();
            } else {
                return ((Number) value).longValue();
            }
        } else if (value instanceof boolean[]) {
            JSONArray array = new JSONArray();
            for (boolean arrValue : (boolean[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof char[]) {
            JSONArray array = new JSONArray();
            for (char arrValue : (char[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof CharSequence[]) {
            JSONArray array = new JSONArray();
            for (CharSequence arrValue : (CharSequence[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof double[]) {
            JSONArray array = new JSONArray();
            for (double arrValue : (double[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof float[]) {
            JSONArray array = new JSONArray();
            for (float arrValue : (float[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof int[]) {
            JSONArray array = new JSONArray();
            for (int arrValue : (int[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof long[]) {
            JSONArray array = new JSONArray();
            for (long arrValue : (long[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof short[]) {
            JSONArray array = new JSONArray();
            for (short arrValue : (short[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        } else if (value instanceof String[]) {
            JSONArray array = new JSONArray();
            for (String arrValue : (String[]) value) {
                array.put(getJSONValue(arrValue));
            }
            return array;
        }
        return null;
    }

    /**
     * Gets a JSONObject equivalent to the input bundle for use when falling back to a web navigation.
     */
    private JSONObject getJSONForBundle(Bundle bundle) throws JSONException {
        JSONObject root = new JSONObject();
        for (String key : bundle.keySet()) {
            root.put(key, getJSONValue(bundle.get(key)));
        }
        return root;
    }

    /**
     * Performs the navigation.
     *
     * @param context the Context from which the navigation should be performed.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public NavigationResult navigate(Context context) {
        PackageManager pm = context.getPackageManager();
        Bundle finalAppLinkData = buildAppLinkDataForNavigation(context);

        Intent eligibleTargetIntent = null;
        for (AppLink.Target target : getAppLink().getTargets()) {
            Intent targetIntent = new Intent(Intent.ACTION_VIEW);
            if (target.getUrl() != null) {
                targetIntent.setData(target.getUrl());
            } else {
                targetIntent.setData(appLink.getSourceUrl());
            }
            targetIntent.setPackage(target.getPackageName());
            if (target.getClassName() != null) {
                targetIntent.setClassName(target.getPackageName(), target.getClassName());
            }
            targetIntent.putExtra(AppLinks.KEY_NAME_APPLINK_DATA, finalAppLinkData);

            ResolveInfo resolved = pm.resolveActivity(targetIntent, PackageManager.MATCH_DEFAULT_ONLY);
            if (resolved != null) {
                eligibleTargetIntent = targetIntent;
                break;
            }
        }

        Intent outIntent = null;
        NavigationResult result = NavigationResult.FAILED;
        if (eligibleTargetIntent != null) {
            outIntent = eligibleTargetIntent;
            result = NavigationResult.APP;
        } else {
            // Fall back to the web if it's available
            Uri webUrl = getAppLink().getWebUrl();
            if (webUrl != null) {
                JSONObject appLinkDataJson;
                try {
                    appLinkDataJson = getJSONForBundle(finalAppLinkData);
                } catch (JSONException e) {
                    sendAppLinkNavigateEventBroadcast(context, eligibleTargetIntent, NavigationResult.FAILED, e);
                    throw new RuntimeException(e);
                }
                webUrl = webUrl.buildUpon()
                        .appendQueryParameter(AppLinks.KEY_NAME_APPLINK_DATA, appLinkDataJson.toString())
                        .build();
                outIntent = new Intent(Intent.ACTION_VIEW, webUrl);
                result = NavigationResult.WEB;
            }
        }

        sendAppLinkNavigateEventBroadcast(context, outIntent, result, null);
        if (outIntent != null) {
            context.startActivity(outIntent);
        }
        return result;
    }

    private void sendAppLinkNavigateEventBroadcast(Context context, Intent intent, NavigationResult type, JSONException e) {
        Map<String, String> extraLoggingData = new HashMap<String, String>();
        if (e != null) {
            extraLoggingData.put("error", e.getLocalizedMessage());
        }

        extraLoggingData.put("success", type.isSucceeded() ? "1" : "0");
        extraLoggingData.put("type", type.getCode());

        MeasurementEvent.sendBroadcastEvent(
                context,
                MeasurementEvent.APP_LINK_NAVIGATE_OUT_EVENT_NAME,
                intent,
                extraLoggingData);
    }

    /**
     * Sets the default resolver to be used for App Link resolution. Setting this to null will cause
     * the {@link #navigateInBackground(Context, Uri)} methods to use the
     * basic, built-in resolver provided by Bolts.
     *
     * @param resolver the resolver to use by default.
     */
    public static void setDefaultResolver(AppLinkResolver resolver) {
        defaultResolver = resolver;
    }

    /**
     * Gets the default resolver to be used for App Link resolution. If the developer has not set a
     * default resolver, this will return {@code null}, but the basic, built-in resolver provided by
     * Bolts will be used.
     *
     * @return the default resolver, or {@code null} if none is set.
     */
    public static AppLinkResolver getDefaultResolver() {
        return defaultResolver;
    }

    private static AppLinkResolver getResolver(Context context) {
        if (getDefaultResolver() != null) {
            return getDefaultResolver();
        }
        return new WebViewAppLinkResolver(context);
    }

    /**
     * Navigates to an {@link AppLink}.
     *
     * @param context the Context from which the navigation should be performed.
     * @param appLink the AppLink being navigated to.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static NavigationResult navigate(Context context, AppLink appLink) {
        return new AppLinkNavigation(appLink, null, null).navigate(context);
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the App Link resolution
     * strategy specified.
     *
     * @param context     the Context from which the navigation should be performed.
     * @param destination the destination URL for the App Link.
     * @param resolver    the resolver to use for fetching App Link metadata.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(final Context context,
                                                              Uri destination,
                                                              AppLinkResolver resolver) {
        return resolver.getAppLinkFromUrlInBackground(destination)
                .onSuccess(new Continuation<AppLink, NavigationResult>() {
                    @Override
                    public NavigationResult then(Task<AppLink> task) throws Exception {
                        return navigate(context, task.getResult());
                    }
                }, Task.UI_THREAD_EXECUTOR);
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the App Link resolution
     * strategy specified.
     *
     * @param context     the Context from which the navigation should be performed.
     * @param destination the destination URL for the App Link.
     * @param resolver    the resolver to use for fetching App Link metadata.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(Context context,
                                                              URL destination,
                                                              AppLinkResolver resolver) {
        return navigateInBackground(context, Uri.parse(destination.toString()), resolver);
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the App Link resolution
     * strategy specified.
     *
     * @param context        the Context from which the navigation should be performed.
     * @param destinationUrl the destination URL for the App Link.
     * @param resolver       the resolver to use for fetching App Link metadata.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(Context context,
                                                              String destinationUrl,
                                                              AppLinkResolver resolver) {
        return navigateInBackground(context, Uri.parse(destinationUrl), resolver);
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the default
     * App Link resolution strategy.
     *
     * @param context     the Context from which the navigation should be performed.
     * @param destination the destination URL for the App Link.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(Context context,
                                                              Uri destination) {
        return navigateInBackground(context,
                destination,
                getResolver(context));
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the default
     * App Link resolution strategy.
     *
     * @param context     the Context from which the navigation should be performed.
     * @param destination the destination URL for the App Link.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(Context context,
                                                              URL destination) {
        return navigateInBackground(context,
                destination,
                getResolver(context));
    }

    /**
     * Navigates to an {@link AppLink} for the given destination using the default
     * App Link resolution strategy.
     *
     * @param context        the Context from which the navigation should be performed.
     * @param destinationUrl the destination URL for the App Link.
     * @return the {@link NavigationResult} performed by navigating.
     */
    public static Task<NavigationResult> navigateInBackground(Context context,
                                                              String destinationUrl) {
        return navigateInBackground(context,
                destinationUrl,
                getResolver(context));
    }
}
